File: /var/www/kevin-demo/wp-content/plugins/grocerslist/includes/Support/SalesPagePattern.php
<?php
namespace GrocersList\Support;
use GrocersList\Service\CreatorSettingsFetcher;
class SalesPagePattern
{
public const PATTERN_NAME = 'grocerslist/sales-page';
private CreatorSettingsFetcher $creatorSettingsFetcher;
public function __construct(CreatorSettingsFetcher $creatorSettingsFetcher)
{
$this->creatorSettingsFetcher = $creatorSettingsFetcher;
}
public function registerPattern(): void
{
if (!function_exists('register_block_pattern')) {
return;
}
register_block_pattern(self::PATTERN_NAME, [
'title' => __('GRO Sales Page', 'grocers-list'),
'description' => __('Membership sales landing page generated by the GRO plugin.', 'grocers-list'),
'categories' => ['call-to-action'],
'content' => $this->buildContent(),
]);
}
/**
* Build the block markup for the sales page, substituting current creator/pricing/CTA values.
* Safe to call whether or not register_block_pattern exists — used by both registration and
* the page-generation flow.
*
* The $ctaUrl override, if provided, replaces the "Join Now" href in all three CTA placements.
* The "Login" href is always the widget login anchor.
*/
public function buildContent(?string $ctaUrl = null): string
{
$joinUrl = $ctaUrl ?: $this->getJoinUrl();
$loginUrl = $this->getLoginUrl();
$pricingSubline = $this->getPricingSubline();
$joinUrlEsc = esc_url($joinUrl);
$loginUrlEsc = esc_url($loginUrl);
$pricingSublineEsc = esc_html($pricingSubline);
$ctaBlock = $this->buildCtaBlock($joinUrlEsc, $loginUrlEsc, $pricingSublineEsc);
return <<<HTML
<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull"><!-- wp:heading {"textAlign":"center","level":1} -->
<h1 class="wp-block-heading has-text-align-center">Join the Membership</h1>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Get an ad-free website experience, member-only content and more exclusive perks!</p>
<!-- /wp:paragraph -->
{$ctaBlock}</div>
<!-- /wp:group -->
<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">What you get</h2>
<!-- /wp:heading -->
<!-- wp:columns -->
<div class="wp-block-columns"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:heading {"textAlign":"center","level":3} -->
<h3 class="wp-block-heading has-text-align-center">An Ad-free experience</h3>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Enjoy all posts on the website without ads.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"><!-- wp:heading {"textAlign":"center","level":3} -->
<h3 class="wp-block-heading has-text-align-center">Member exclusive content</h3>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Access a growing library of content just for members.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->
<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">What members are saying</h2>
<!-- /wp:heading -->
<!-- wp:quote -->
<blockquote class="wp-block-quote"><!-- wp:paragraph -->
<p>Blah blah quote same as before</p>
<!-- /wp:paragraph --></blockquote>
<!-- /wp:quote -->
<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">{$ctaBlock}</div>
<!-- /wp:group -->
<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">Frequently Asked Questions</h2>
<!-- /wp:heading -->
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">How does the membership work?</h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Register once using your email address and password, and you'll automatically begin your ad-free member experience.</p>
<!-- /wp:paragraph -->
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Can I cancel anytime?</h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>You can cancel any time. Stay as long as it serves you!</p>
<!-- /wp:paragraph -->
<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull">{$ctaBlock}</div>
<!-- /wp:group -->
HTML;
}
private function buildCtaBlock(string $joinUrlEsc, string $loginUrlEsc, string $pricingSublineEsc): string
{
return <<<HTML
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons"><!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="{$joinUrlEsc}">Join Now</a></div>
<!-- /wp:button -->
<!-- wp:button {"className":"is-style-outline"} -->
<div class="wp-block-button is-style-outline"><a class="wp-block-button__link wp-element-button" href="{$loginUrlEsc}">Login</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->
<!-- wp:paragraph {"align":"center","fontSize":"small"} -->
<p class="has-text-align-center has-small-font-size">{$pricingSublineEsc}</p>
<!-- /wp:paragraph -->
HTML;
}
private function getPriceMonthly(): string
{
$settings = $this->creatorSettingsFetcher->getCreatorSettings();
$price = $settings->settings->memberships->priceMonthly ?? 0;
return $this->formatPrice($price, '$X');
}
private function getPriceYearly(): string
{
$settings = $this->creatorSettingsFetcher->getCreatorSettings();
$price = $settings->settings->memberships->priceYearly ?? 0;
return $this->formatPrice($price, '$Y');
}
private function formatPrice($price, string $fallback): string
{
if (!is_numeric($price) || (float) $price <= 0) {
return $fallback;
}
return '$' . number_format(((float) $price) / 100, 2);
}
private function getPricingSubline(): string
{
$monthlyRaw = $this->getRawPrice('priceMonthly');
$yearlyRaw = $this->getRawPrice('priceYearly');
$hasMonthly = $monthlyRaw !== null;
$hasYearly = $yearlyRaw !== null;
if (!$hasMonthly && !$hasYearly) {
return '$X/mo or $Y/year (Z% off)';
}
if ($hasMonthly && !$hasYearly) {
return $this->formatPrice($monthlyRaw, '$X') . '/mo';
}
if (!$hasMonthly && $hasYearly) {
return $this->formatPrice($yearlyRaw, '$Y') . '/year';
}
$monthlyTotal = $monthlyRaw * 12;
if ($monthlyTotal <= 0 || $yearlyRaw >= $monthlyTotal) {
return $this->formatPrice($monthlyRaw, '$X') . '/mo or ' . $this->formatPrice($yearlyRaw, '$Y') . '/year';
}
$discount = (int) round((($monthlyTotal - $yearlyRaw) / $monthlyTotal) * 100);
if ($discount <= 0) {
return $this->formatPrice($monthlyRaw, '$X') . '/mo or ' . $this->formatPrice($yearlyRaw, '$Y') . '/year';
}
return $this->formatPrice($monthlyRaw, '$X') . '/mo or ' . $this->formatPrice($yearlyRaw, '$Y') . '/year (' . $discount . '% off)';
}
private function getRawPrice(string $field): ?float
{
$settings = $this->creatorSettingsFetcher->getCreatorSettings();
$price = $settings->settings->memberships->{$field} ?? null;
if (!is_numeric($price) || (float) $price <= 0) {
return null;
}
return (float) $price;
}
// Widget hook: BottomDrawer auto-opens the join/signup screen on hashchange.
private function getJoinUrl(): string
{
return '#memberships-open';
}
// Widget hook: BottomDrawer opens directly to the Login screen on hashchange.
private function getLoginUrl(): string
{
return '#memberships-login';
}
// Backwards-compat alias — pre-redesign callers / tests may still reference this name.
private function getCheckoutUrl(): string
{
return $this->getJoinUrl();
}
}